<?php
/**
 * Library Of Shared Code (LOSC)
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Framework
 * @package    Controller
 * @subpackage Crud
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * Base controller for CRUD operations on Doctrine records
 *
 * @category   LOSC Framework
 * @package    Controller
 * @subpackage Crud
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
abstract class Losc_Controller_Crud_DoctrineController
    extends Zym_Controller_Action_Abstract
{
    /**#@+
     * General controller property.
     */
    
    /**
     * Action contexts for context switching
     *
     * @var array
     */
    public $contexts = array(
        'find'    => array('html', 'json', 'xml'),
        'create'  => array('html', 'json', 'xml'),
        'read'    => array('html', 'json', 'xml'),
        'update'  => array('html', 'json', 'xml'),
        'delete'  => array('html', 'json', 'xml'),
        'result'  => array('html', 'json', 'xml'),
        'new'     => array('html')
    );
    
    /**
     * Output format (for context switching)
     *
     * @var string
     */
    public $format;
    
    /**
     * CRUD session
     *
     * @var Zend_Session_Namespace
     */
    protected $_session;
    
    /**#@- ------------------------------------------------------------------ */
    
    /**#@+
     * CRUD related property.
     */
    
    /**
     * Whether default view scripts should be used
     *
     * @var boolean
     */
    private $_useDefaultViewScripts = true;
    
    /**
     * Table instance
     *
     * @var Doctrine_Table
     */
    private $_table;
    
    /**
     * Caching mechanism for forms
     *
     * @var array
     */
    private $_forms;
    
    /**
     * Fields that shouldn't be exposed
     *
     * @var array
     */
    private $_hiddenFields = array('deleted');
    
    /**
     * Relations that shouldn't be exposed
     *
     * @var array
     */
    private $_hiddenRelations = array();
    
    /**#@- ------------------------------------------------------------------ */
    
    /**#@+
     * General controller method.
     */
    
    /**
     * Initializes controller
     *
     * @return void
     */
    public function init()
    {
        // call parent for good measure
        parent::init();
        
        // init CRUD session
        $this->_session = new Zend_Session_Namespace(get_class($this));
        
        // init Doctrine table
        $this->_table = Doctrine::getTable($this->getTableName());
        
        // get format (for context switching)
        $this->format = $this->getRequest()->getParam('format', 'html');
        
        // get context switcher
        $cs = $this->getHelper('ContextSwitch');
        
        // add html context
        $cs->setContext('html', array(
            'suffix'  => '',
            'headers' => array('Content-Type' => 'text/html; charset=UTF-8')
        ));
        
        // disable automatic json serialization
        $cs->setAutoJsonSerialization(false);
        
        // TODO: remove when not debugging
        $cs->setHeader('json', 'Content-Type', 'text/javascript');
    }
    
    /**#@- ------------------------------------------------------------------ */
    
    /**#@+
     * CRUD action.
     */
    
    /**
     * Convenience action for showing a form for generating a new model
     *
     * @return void
     */
    public function newAction()
    {
        $request    = $this->getRequest();
        $module     = $request->getModuleName();
        $controller = $request->getControllerName();
        
        $url = $this->getHelper('Url')->url(array(
            'module' => $module,
            'controller' => $controller,
            'action' => 'create'
        ), 'default', true);
        
        $form = $this->getForm();
        $form->setAction($url);
        
        $this->view->form = $form;
        
        $this->_renderCrud();
    }
    
    /**
     * Creates a single record
     *
     * @return void
     */
    public function createAction()
    {
        if (!$this->getRequest()->isPost()) {
            $this->_result(Losc_Controller_Crud_Result::ERROR_BAD_HTTP_METHOD);
            return;
        }
        
        // TODO: implement
        echo 'createAction()<br/>';
        var_dump($_POST);
        //exit;
        
        $form = $this->getForm();
        if ($form->isValid($_POST)) {
            $record = $this->_table->getRecord();
        
            try {
                $success = $form->save($record);
                
                if ($success) {
                    $code = Losc_Controller_Crud_Result::SUCCESS_CREATED;
                    $this->_result($code, null, $record);
                    return;
                } else {
                    $code = Losc_Controller_Crud_Result::ERROR_OTHER;
                    $this->_result($code);
                    return;
                }
            } catch (Losc_Record_ValidationException $lrve) {
                $code = Losc_Controller_Crud_Result::ERROR_INVALID_PARAMS;
                $messages = array();
            
                $t = $this->getHelper('Translator');
                
                foreach ($lrve->getMessages() as $name => $errorMessages) {
                    if ($element = $form->getElement($name)) {
                        $label = $form->getElement($name)->getLabel();
                        $label = $t->translate($label);
                        foreach ($errorMessages as $error) {
                            $messages[] = $label . ': ' . $error;
                        }
                    } elseif (is_array($errorMessages)) {
                        foreach ($errorMessages as $error) {
                            $messages[] = $error;
                        }
                    } elseif (is_string($errorMessages)) {
                        $messages[] = $errorMessages;
                    }
                }
                
                $this->_result($code, null, $messages);
                return;
            } catch (Losc_Exception $le) {
                $code = Losc_Controller_Crud_Result::ERROR_APPLICATION;
                $msg = $le->getLocalizedMessage();
                $this->_result($code, $msg);
                return;
            } catch (Doctrine_Connection_Exception $dce) {
                $code = Losc_Controller_Crud_Result::ERROR_SYSTEM;
                $msg = array('Database error (%s).', $dce->getPortableMessage());
                $this->_result($code, $msg, array($dce->getMessage()));
                return;
            } catch (Doctrine_Exception $de) {
                $em = $de->errorMessage($de->getCode());
                $code = Losc_Controller_Crud_Result::ERROR_SYSTEM;
                $msg = array('Database error (%s).', $em);
                $this->_result($code, $msg, array($de->getMessage()));
                return;
            } catch (Exception $e) {
                $code = Losc_Controller_Crud_Result::ERROR_SYSTEM;
                $this->_result($code, get_class($e), array($e->getMessage()));
                return;
            }
        } else {
            $code = Losc_Controller_Crud_Result::ERROR_INVALID_PARAMS;
            $messages = array();
            
            $t = $this->getHelper('Translator');
            
            foreach ($form->getMessages() as $name => $errorMessages) {
                if ($element = $form->getElement($name)) {
                    $label = $form->getElement($name)->getLabel();
                    $label = $t->translate($label);
                    foreach ($errorMessages as $error) {
                        $messages[] = $label . ': ' . $error;
                    }
                } elseif (is_array($errorMessages)) {
                    foreach ($errorMessages as $error) {
                        $messages[] = $error;
                    }
                } elseif (is_string($errorMessages)) {
                    $messages[] = $errorMessages;
                }
            }
            
            $this->_result($code, null, $messages);
            return;
        }
    }
    
    /**
     * Updates a single record
     *
     * @return void
     */
    public function updateAction()
    {
        if (!$this->getRequest()->isPost()) {
            $this->_result(Losc_Controller_Crud_Result::ERROR_BAD_HTTP_METHOD);
            return;
        }
        
        // TODO: implement
        echo 'updateAction()<br/>';
        
        var_dump($_POST);
    }
    
    /**
     * Deletes one record
     *
     * @return void
     */
    public function deleteAction()
    {
        if (!$this->getRequest()->isPost()) {
            $this->_result(Losc_Controller_Crud_Result::ERROR_BAD_HTTP_METHOD);
            return;
        }
        
        // TODO: implement
        echo 'deleteAction()<br/>';
        
        var_dump($_POST);
    }
    
    public function testAction()
    {
        header('Content-Type: text/html; charset=UTF-8');
        
        $pattern = $this->_getBelongsTo();

        foreach ($_POST as $key => $data) {
            if (preg_match($pattern, $key, $match)) {
                
                array_shift($match);
                $where = array();
                
                for ($i = 0; $i < count($match); $i += 2) {
                    $where[$match[$i]] = array($match[$i + 1]);
                }
                
                $result = $this->_fetch($where);
                //$form = $this->getForm($result[0]);
                
                $form = $this->getForm();
                $form->setElementsBelongTo($this->_getBelongsTo($result[0]));
                
                if (!is_array($data)) {
                    throw new Exception('blabla POST does not contain data');
                }
                
                $form->populate($data);
                
                echo $form->render();
                exit;
                break;
            }
        }
        
        echo '<pre>';
        $this->dump($_POST, '$_POST');
        echo '</pre>';
        exit;
    }
    
    /**
     * Retrieves a single record, where the identifier must be specified as a
     * param in GET or POST
     *
     * @return void
     */
    public function readAction()
    {
        // where to retrieve id from
        $idSource = $this->getRequest()->isPost() ? $_POST : $_GET;
        
        $where = array();
        
        $tableIdentifiers = (array) $this->_table->getIdentifier();
        
        foreach ($tableIdentifiers as $tableIdentifier) {
            if (!isset($idSource[$tableIdentifier])) {
                $code = Losc_Controller_Crud_Result::ERROR_INVALID_PARAMS;
                $msg = 'Unable to retrieve data. Identifier not given (%s).';
                $this->_result($code, array($msg, $tableIdentifier));
                return;
            }
            
            $where[$tableIdentifier] = array($idSource[$tableIdentifier]);
        }
        
        $data = $this->_fetch($where);
        
        if (count($data) < 1) {
            $code = Losc_Controller_Crud_Result::ERROR_NOT_FOUND;
            $msg = 'No object found.';
        } else {
            $code = Losc_Controller_Crud_Result::SUCCESS_FOUND;
            $msg = 'Found one object.';
            $msg = null;
        }
        
        $this->_result($code, $msg, $data);
    }
    
    /**
     * Retrieves several records, using where clauses given from GET or POST
     *
     * @return void
     */
    public function findAction()
    {
        // where to retrieve where clauses from
        $whereSource = $this->getRequest()->isPost() ? $_POST : $_GET;
        
        // determine where clauses
        if (isset($whereSource['where'])) {
            if (is_array($whereSource['where'])) {
                $where = $whereSource['where'];
            } else {
                $where = array();
            }
        } else {
            $where = array();
        }
        
        $data = $this->_fetch($where, true);
        $count = count($data);
        
        if ($count < 1) {
            $code = Losc_Controller_Crud_Result::ERROR_NOT_FOUND;
            $msg = 'No objects found.';
        } elseif ($count > 1) {
            $code = Losc_Controller_Crud_Result::SUCCESS_FOUND;
            $msg = array('Found %s objects.', $count);
        } else {
            $code = Losc_Controller_Crud_Result::SUCCESS_FOUND;
            $msg = 'Found one object.';
        }
        
        $this->_result($code, $msg, $data);
    }
    
    /**
     * Sinkhole action: user is sent here after a CRUD operation
     *
     * @return void
     */
    public function resultAction()
    {
        // disable the view renderer (we have no view script)
        $this->getHelper('ViewRenderer')->setNoRender();
        
        // get result from session and remove it
        $result = $this->_session->result;
        unset($this->_session->result);
        
        // verify result
        if (!$result instanceof Losc_Controller_Crud_Result) {
            // no result, meaning action was probably called manually
            $code = Losc_Controller_Crud_Result::ERROR_INVALID_REQUEST;
            $msg = $this->getHelper('Translator')->translate('No result.');
            $result = new Losc_Controller_Crud_Result($code, $msg);
        }

        // get response
        $response = $this->getResponse();
        
        // set response code from result
        $response->setHttpResponseCode($result->getHttpCode());
        
        $this->_renderCrud();
        return;
        
        // serialize result according to format
        switch (strtolower($this->format)) {
            case 'json':
                $body = $this->_resultToJson($result);
                break;
            case 'xml':
                $body = $this->_resultToXml($result);
                break;
            default:
                $body = $this->_resultToHtml($result);
                break;
        }
        
        echo $body;
        return;
        
        // set response body
        $response->setBody($body);
        
        // send response
        $response->sendResponse();
    }
    
    /**#@- ------------------------------------------------------------------ */
    
    /**#@+
     * CRUD helper method.
     */
    
    /**
     * Renders view script for CRUD action
     *
     * @return void
     */
    private function _renderCrud()
    {
        if ($this->getUseDefaultViewScripts()) {
            $vr = $this->getHelper('ViewRenderer');
            
            // store state
            $oldAction          = $vr->getScriptAction();
            $oldResponseSegment = $vr->getResponseSegment();
            $oldNoController    = $vr->getNoController();
            $oldScriptPaths     = $vr->view->getScriptPaths();
            
            // render default script
            $vr->view->setScriptPath(dirname(__FILE__) . '/Data');
            $vr->render(null, null, true);
            
            // restore state
            $vr->setRender($oldAction, $oldResponseSegment, $oldNoController);
            $vr->view->setScriptPath($oldScriptPaths);
        }
    }
    
    /**
     * Ends CRUD with a result
     *
     * @param  boolean      $success  whether operation was successful
     * @param  string|array $message  failure message, with value substitutions
     * @param  array        $data     response data
     * @return void
     */
    private function _result($code, $message = null, array $data = null)
    {
        // create result object
        $result = new Losc_Controller_Crud_Result($code, $message, $data);
        $result->hideField($this->_hiddenFields);
        $result->hideRelation($this->_hiddenRelations);
        
        // store it in session so it persists between requests
        $this->_session->result = $result;
        
        // determine if we should redirect or call the result action directly
        $request = $this->getRequest();
        if ($request->isPost() && !$request->isXmlHttpRequest()) {
            $this->_goto('result');
        } else {
            $this->resultAction();
        }
    }
    
    /**
     * Fetches records matching where clauses from POST
     *
     * @param  array   $where   [optional] an array of where clauses to put in 
     *                          the query
     * @param  boolean $all     [optional] whether all records matching should
     *                          be fetched, or just one. Default is to fetch
     *                          one.
     * @return array
     */
    private function _fetch(array $where = array(), $all = false)
    {
        // create query
        $q = $this->_table->createQuery();
        
        // add where clauses
        $q = $this->_addWhereClausesFromArray($q, $where);
        
        // event hook
        $this->preFetch($q);
        
        // set hydration mode
        $q->setHydrationMode(Doctrine::HYDRATE_RECORD);
        
        if (!$all) {
            $q->limit(1);
        }
        
        // determine fields to retrieve
        $fieldNames = $this->_table->getFieldNames();
        $selectFields = array_diff($fieldNames, $this->_hiddenFields);
        $q->select(implode(',', $selectFields));
        
        try {
            $result = $q->execute()->getData();
            
            foreach ($this->_table->getRelations() as $alias => $relation) {
                $definition = $relation->toArray();
                if (!empty($definition['refTable'])) {
                    $this->_hiddenRelations[] =
                        $definition['refTable']->getOption('name');
                }
            
                if (in_array($alias, $this->_hiddenRelations)) {
                    continue;
                }
            
                foreach ($result as $record) {
                    $record->loadReference($alias);
                }
            }
            
            return $result;
            
            // TODO: log exceptions?
        } catch (Doctrine_Connection_Exception $dce) {
            $m = array('Database error (%s).', $dce->getPortableMessage());
            $this->_result(Losc_Controller_Crud_Result::ERROR_APPLICATION, $m);
        } catch (Doctrine_Exception $de) {
            $code = $de->getCode();
            if ($code) {
                $m = $de->errorMessage($code);
            } else {
                $m = $de->getMessage();
            }
            $m = array('Database error (%s).', $m);
            $this->_result(Losc_Controller_Crud_Result::ERROR_INTERNAL, $m);
        } catch (Losc_Exception $le) {
            $m = $le->getLocalizedMessage();
            $this->_result(Losc_Controller_Crud_Result::ERROR_APPLICATION,
                           null, array($m));
        } catch (Exception $e) {
            $m = array('Internal error (%s).', get_class($e));
            $this->_result(Losc_Controller_Crud_Result::ERROR_INTERNAL,
                           $m, array($m, $e->getMessage()));
        }
    }
    
    /**
     * Extracts 'where' clauses from the given array and adds them to $q
     *
     * @param  Doctrine_Query $query  query to add where clauses to
     * @param  array          $data   array to extract where clauses from
     * @return Doctrine_Query
     */
    private function _addWhereClausesFromArray(Doctrine_Query $query, array $data)
    {
        $validOperators = array('=', '<', '>', '<=', '>=', '<>', '!=',
                                'IS NULL', 'IS NOT NULL', 'LIKE');
        
        $code = Losc_Controller_Crud_Result::ERROR_INVALID_PARAMS;
        
        // loop specified fields
        foreach ($data as $field => $clauses) {
            // first, spend some time validating field/clauses
            if ($this->_table->hasRelation($field)) {
                if (!$this->_table->hasField($field)) {
                    $msg = 'Unable to filter data (cannot use relation "%s").';
                    $this->_result($code, array($msg, $field));
                    return;
                }
            } elseif (!$this->_table->hasField($field)) {
                $msg = 'Unable to filter data (invalid field "%s").';
                $this->_result($code, array($msg, $field));
                return;
            } elseif (!is_array($clauses)) {
                $msg = 'Unable to filter data (invalid "where" for field "%s").';
                $this->_result($code, array($msg, $field));
                return;
            }
            
            // loop the field's where clauses
            foreach ($clauses as $op => $value) {
                $op = strtoupper($op);
                if (is_numeric($op)) {
                    $op = '=';
                } elseif (!in_array($op, $validOperators)) {
                    $msg = 'Unable to filter data (invalid operator "%s").';
                    $this->_result($code, array($msg, $op));
                    return;
                }
                
                // good to go, add the where clause
                $query->addWhere("$field $op ?", array($value));
            }
        }
        
        return $query;
    }
    
    /**
     * Returns a HTML string of the result
     *
     * @param  Losc_Controller_Crud_Result $result
     * @return string
     */
    private function _resultToHtml(Losc_Controller_Crud_Result $result)
    {
        $this->view->crudResult = $result;
        if ($result->isSuccess()) {
            $html = "<div class=\"crud-result crud-success\">\n"
                  . "    <h3>{$result->getMessage()}</h3>\n"
                  . "    <div class=\"data\">\n";
            
            $acl        = Losc_Security_Acl_Doctrine::getInstance();
            $request    = $this->getRequest();
            $module     = $request->getModuleName();
            $controller = $request->getControllerName();
            
            // check if allowed to update model
            if ($acl->isAllowedAction($module, $controller, 'update')) {
                // action url for form
                $updateUrl = $this->getHelper('Url')->url(array(
                    'module' => $module,
                    'controller' => $controller,
                    'action' => 'update'
                ), 'default', true);
                
                // check if allowed to delete model
                $del = $acl->isAllowedAction($module, $controller, 'delete');
                
                // loop records from result
                foreach ($result->getData(false, true) as $record) {
                    // get form for record
                    $form = $this->getForm($record, $del);
                    $form->setAction($updateUrl);
                    
                    // add form to html
                    $html .= "        <div class=\"record\">\n"
                           .              $form->render() . "\n"
                           . "        </div>\n";
                }
            } else {
                // not allowed to update model; render regular html
                // TODO: render non-form html
            }
            
            $html .= "    </div>\n"
                   . "</div>\n";
        } else {
            $html = "<div class=\"crud-result crud-failure\">\n"
                  . "    <h3>{$result->getMessage()}</h3>\n";
            
            $errors = '';
            foreach ($result->getData() as $message) {
                $errors .= "        <li>$message</li>\n";
            }
            
            if ($errors) {
                $html .= "    <ul class=\"errors\">\n"
                       .          $errors
                       . "    </ul>\n";
            }
            
            $html .= "</div>\n";
        }
        
        return $html;
    }
    
    /**
     * Returns a JSON string of the result
     *
     * @param  Losc_Controller_Crud_Result $result
     * @return string
     */
    private function _resultToJson(Losc_Controller_Crud_Result $result)
    {
        $array = $result->toArray();
        unset($array['httpCode']);
        return Zend_Json::encode($array);
    }
    
    /**
     * Returns an XML string of the result
     *
     * @param  Losc_Controller_Crud_Result $result
     * @return string
     */
    private function _resultToXml(Losc_Controller_Crud_Result $result)
    {
        $doc = $this->_resultToDomDocument($result);
        $doc->formatOutput = true;
        return $doc->saveXML();
    }
    
    /**
     * Returns a DOMDocument instance representing the result
     *
     * @param  Losc_Controller_Crud_Result $result
     * @return DOMDocument
     */
    private function _resultToDomDocument(Losc_Controller_Crud_Result $result)
    {
        $doc = new DOMDocument('1.0', 'UTF-8');
        
        $rootNode = $doc->createElement('result');
        $doc->appendChild($rootNode);
        
        $codeNode = $doc->createElement('code', $result->getCode());
        $rootNode->appendChild($codeNode);
        
        $messageNode = $doc->createElement('message', $result->getMessage());
        $rootNode->appendChild($messageNode);
        
        $dataNode = $doc->createElement('data');
        
        if ($result->isSuccess()) {
            $this->_arrayToXml($result->getData(true), $doc, $dataNode);
        } else {
            foreach ($result->getData() as $errorMessage) {
                $m = $doc->createElement('message', $errorMessage);
                $dataNode->appendChild($m);
            }
        }
        
        $rootNode->appendChild($dataNode);
        
        return $doc;
    }
    
    /**
     * Adds an array to the given $parent
     *
     * @param  array       $array
     * @param  DOMDocument $doc
     * @param  DOMElement  $parent
     * @return void
     */
    private function _arrayToXml($array, DOMDocument $doc, DOMElement $parent)
    {
        foreach ($array as $key => $value) {
            if ($pos = strrpos($key, '_')) {
                $lastPart = substr($key, $pos + 1);
                if (is_numeric($lastPart)) {
                    $name = substr($key, 0, $pos);
                } else {
                    $name = $key;
                }
            } else {
                $name = $key;
            }
            
            $rootNode = $doc->createElement($name);
            
            if (is_array($value)) {
                $this->_arrayToXml($value, $doc, $rootNode);
            } else {
                $value = htmlspecialchars($value);
                $textNode = $doc->createTextNode($value);
                $rootNode->appendChild($textNode);
            }
            
            $parent->appendChild($rootNode);
        }
    }
    
    /**
     * Returns an "elementsBelongTo" name or pattern
     *
     * @param  Doctrine_Record $record
     * @return string
     */
    private function _getBelongsTo(Doctrine_Record $record = null)
    {
        $belongsTo = '';
        
        $ids = (array) $this->_table->getIdentifier();
        
        if ($record) {
            foreach ($ids as $key => $id) {    
                $belongsTo .= "\xae" . $id . "\xa6" . $record[$id];
            }
        } else {
            foreach ($ids as $key => $id) {    
                $belongsTo .= "\xae($id)\xa6(.+)";
            }
            $belongsTo = "/$belongsTo/";
        }
        
        return utf8_encode($belongsTo);
    }
    
    /**
     * Generates the form first call, and returns a clone on subsequent calls
     *
     * @param  boolean $deletable
     * @return Losc_Form_Doctrine
     */
    private function _getCachedForm($deletable)
    {
        if ($deletable) {
            if (isset($this->_forms['deletable'])) {
                return clone $this->_forms['deletable'];
            }
            
            // get form
            $form = $this->_getCachedForm(false);
            
            // translate label
            $label = 'Delete';
            $label = $this->getHelper('Translator')->translate($label);
            $label .= ' ' . strtolower($form->getDescription());
            
            // create checkbox for deletion
            $delCheckbox = new Zend_Form_Element_Checkbox('deleted');
            $delCheckbox->setLabel($label);
            $delCheckbox->setDescription('&nbsp;');
            $delCheckbox->setDecorators(array(
                'ViewHelper',
                'Errors',
                array('Label', array(
                    'tag' => 'span',
                    'separator' => '',
                    'placement' => 'APPEND'
                )),
                array('HtmlTag', array(
                    'tag' => 'dd'
                )),
                array('Description', array(
                    'tag' => 'dt',
                    'escape' => false,
                    'class' => 'empty',
                    'placement' => 'PREPEND'
                ))
            ));
            
            $form->addElement($delCheckbox);
            
            // cache form
            $this->_forms['deletable'] = $form;
        } else {
            if (isset($this->_forms['normal'])) {
                return clone $this->_forms['normal'];
            }
            
            // create form
            $form = new Losc_Form_Doctrine();
            $form->setMethod('post');
            
            // ignore columns
            foreach ($this->_hiddenFields as $field) {
                if (!$this->_table->hasField($field)) {
                    continue;
                }
                $form->ignoreColumn($this->_table->getColumnName($field));
            }
            
            // ignore relations
            foreach ($this->_hiddenRelations as $relation) {
                $form->ignoreRelation($relation);
            }
            
            // generate from table
            $form->generateFromTable($this->_table);
            
            // cache the form instance
            $this->_forms['normal'] = $form;    
        }
        
        // return clone of generated form
        return clone $form;
    }
    
    /**
     * Returns a form for the optionally, optionally populated from the given
     * $record
     *
     * @param  Doctrine_Record    $record     [optional] record to populate form
     *                                        from
     * @param  boolean            $deletable  [optinonal] if true, a checkbox
     *                                        for deleting will be added
     * @return Losc_Form_Doctrine
     */
    public function getForm(Doctrine_Record $record = null, $deletable = false)
    {
        if ($record) {
            $form = $this->_getCachedForm($deletable);
            $form->setElementsBelongTo($this->_getBelongsTo($record));
            $form->populateFromRecord($record);
        } else {
            $form = $this->_getCachedForm(false);
            //$form->setElementsBelongTo('new');
        }
        
        return $form;
    }
    
    /**#@- ------------------------------------------------------------------ */
    
    /**#@+
     * CRUD related method.
     */
    
    /**
     * Sets a flag indicating whether default view scripts should be used
     *
     * @param  boolean $flag
     * @return Losc_Controller_Crud_DoctrineController
     */
    public function setUseDefaultViewScripts($flag)
    {
        $this->_useDefaultViewScripts = (bool) $flag;
        return $this;
    }
    
    /**
     * Returns a flag indicating whether default view scripts should be used
     *
     * @return boolean
     */
    public function getUseDefaultViewScripts()
    {
        return $this->_useDefaultViewScripts;
    }
    
    /**
     * Hides field(s)
     *
     * @param  string|array $field  field name or array of field names
     * @return Losc_Controller_Crud_DoctrineController
     */
    public function hideField($field)
    {
        if (is_array($field)) {
            $this->_hiddenFields = array_merge($this->_hiddenFields, $field);
        } else {
            $this->_hiddenFields[] = $field;
        }
        
        return $this;
    }
    
    /**
     * Hides relation(s)
     *
     * @param  string|array $relation  relation name or array of relation names
     * @return Losc_Controller_Crud_DoctrineController
     */
    public function hideRelation($relation)
    {
        if (is_array($relation)) {
            $this->_hiddenRelations = array_merge($this->_hiddenRelations,
                                                  $relation);
        } else {
            $this->_hiddenRelations[] = $relation;
        }
        
        return $this;
    }
    
    /**
     * Hides all relations
     *
     * @return Losc_Controller_Crud_DoctrineController
     */
    public function hideAllRelations()
    {
        $this->_hiddenRelations = array_keys($this->_table->getRelations());
        return $this;
    }
    
    /**#@- ------------------------------------------------------------------ */
    
    /**#@+
     * Event hook method for concrete controllers.
     */
    
    /**
     * This method is called prior to fetching records
     *
     * @param  Doctrine_Query $query  query that's about to be executed
     * @return void
     */
    public function preFetch(Doctrine_Query $query) { }
    
    /**
     * This method is called prior to inserting a record in createAction()
     *
     * @param  Doctrine_Record $record  record about to be inserted
     * @return void|boolean             return false to prevent the record from 
     *                                  being inserted
     */
    public function preInsert(Doctrine_Record $record) { }
    
    /**
     * This method is called prior to updating a record in updateAction()
     *
     * @param  Doctrine_Record $record  record that's about to be updated
     * @return void|boolean             return false to prevent the record from 
     *                                  being updated
     */
    public function preUpdate(Doctrine_Record $record) { }
    
    /**
     * This method is called prior to deleting a record in deleteAction()
     *
     * @param  Doctrine_Record $record  record that's about to be deleted
     * @return void|boolean             return false to prevent the record from 
     *                                  being deleted
     */
    public function preDelete(Doctrine_Record $record) { }
    
    /**#@- ------------------------------------------------------------------ */
    
    /**#@+
     * Must be implemented in concrete controllers.
     */
    
    /**
     * Returns table name to use for CRUD operations
     *
     * @return string
     */
    abstract public function getTableName();
    
    /**#@- ------------------------------------------------------------------ */
}
